package org.grayrabbit.cms.plugin;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.grayrabbit.cms.extension.Startable;
import org.grayrabbit.cms.plugin.FilePluginJar;
import org.grayrabbit.cms.plugin.ModuleDescriptorFactory;
import org.grayrabbit.cms.plugin.Plugin;
import org.grayrabbit.cms.plugin.PluginAccessor;
import org.grayrabbit.cms.plugin.PluginException;
import org.grayrabbit.cms.plugin.PluginJar;
import org.grayrabbit.cms.plugin.PluginManagerState;
import org.grayrabbit.cms.plugin.PluginParseException;
import org.grayrabbit.cms.plugin.PluginStateStore;
import org.grayrabbit.cms.plugin.loaders.ClassLoadingPluginLoader;
import org.grayrabbit.cms.plugin.loaders.DefaultPluginFactory;
import org.grayrabbit.cms.plugin.loaders.PluginLoader;
import org.grayrabbit.cms.plugin.parsers.DescriptorParser;
import org.grayrabbit.cms.plugin.parsers.DescriptorParserFactory;
import org.grayrabbit.cms.plugin.parsers.XmlDescriptorParserFactory;
import org.grayrabbit.cms.properties.ApplicationProperties;
import org.grayrabbit.cms.propertyset.CmsPropertySetFactory;
import org.grayrabbit.cms.utils.IOUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.opensymphony.module.propertyset.PropertySet;

public class DefaultPluginInstallationManager implements
		PluginInstallationManager,Startable {
	private static final Logger log = LoggerFactory.getLogger(DefaultPluginInstallationManager.class);
	private boolean started = false;
	private final ApplicationProperties appProperties;
	private final PropertySet pluginsUpdateproperties;
	private final PluginStateStore store;
	
	private final DescriptorParserFactory descriptorParseFactory = new XmlDescriptorParserFactory();
	
	private static final FileFilter jarFileFilter = new FileFilter() {
		public boolean accept(File file) {
			return file.isFile() && file.getName().endsWith(".jar");
		}
	};
	
	public DefaultPluginInstallationManager(ApplicationProperties appProperties,CmsPropertySetFactory propertySetFactory,PluginStateStore store){
		this.appProperties = appProperties;
		this.pluginsUpdateproperties = propertySetFactory.buildCachingPropertySet("cms.plugin.updates", true);
		this.store = store;
		start();
	}
	public void cancelPluginUpdate(String pluginKey) {

	}

	public String installPlugin(PluginAccessor pluginAccessor,
			PluginJar pluginJar) {
		return null;
	}

	public boolean isConfigured() {
		return this.appProperties.getString("cms.plugins") != null;
	}
	
	public void configurePluginPath(String pluginsPath)
	  {
	    File pluginDirectory = new File(pluginsPath);
	    this.appProperties.setString("cms.plugins", pluginDirectory.getPath());
	    start();
	  }


	public void processUpdates() throws PluginParseException, PluginException {
		if(! this.started){
			return;
		}
		log.debug("正在进行插件更新...");
		uninstallPlugins();
		installPlugins();
		
	}

	private void installPlugins() throws PluginException, PluginParseException {
		File pendingPluginsDirectory = getPendingPluginsDirectory();
	    File installedPluginsDirectory = getInstalledPluginsDirectory();
	    File[] pendingPluginFiles = pendingPluginsDirectory.listFiles(jarFileFilter);
	    for (int i = 0; i < pendingPluginFiles.length; i++)
	    {
	      File targetPluginFile = new File(installedPluginsDirectory, pendingPluginFiles[i].getName());
	      try
	      {
	        String key = getKeyFrom(pendingPluginFiles[i]);
	        if (log.isInfoEnabled())
	        {
	          Map<Object,Object> update = getPluginUpdate(key);
	          if ((update == null) || ("install".equals(update.get("type"))))
	          {
	            log.info("Installing plugin '{}' from '{}'",key,targetPluginFile.getAbsolutePath());
	          }
	          else
	          {
	            log.info("Upgrading plugin '{}' with '{}'",key,targetPluginFile.getAbsolutePath());
	          }
	        }
	        targetPluginFile.createNewFile();
	         log.debug("从 '{}' 复制文件到 '{}'", pendingPluginFiles[i].getAbsolutePath(),targetPluginFile.getAbsolutePath());
	        IOUtil.copy(pendingPluginFiles[i], targetPluginFile);
	         log.debug("Deleting plugin file '{}' from pending directory",pendingPluginFiles[i].getAbsolutePath() );
	        pendingPluginFiles[i].delete();

	        if (this.pluginsUpdateproperties.exists(key))
	        {
	            log.debug("Removing update information for plugin {}' from property set",key);
	            this.pluginsUpdateproperties.remove(key);
	        }
	      }
	      catch (IOException e)
	      {
	        throw new PluginException("Error while installing plugin file " + targetPluginFile.getPath(), e);
	      }
	    }
	}
	
	private void uninstallPlugins() throws PluginParseException, PluginException {
		Map<String,Map<Object,Object>> updatesMap = getUpdatesMap();
		Map<String,File> installedPluginJars = getInstalledPluginJars();
		for(String key : updatesMap.keySet()){
			Map<Object,Object> props = updatesMap.get(key);
			if(!"install".equals(props.get("type"))){//如果不是安装类型
				File pluginFile = (File)installedPluginJars.get(key);
				try{
					if(log.isDebugEnabled()){
						if ("uninstall".equals(props.get("type")))
			            {
			              log.info("Uninstalling plugin '{}' by moving the jar file '{}' to the uninstalled plugins directory",key,pluginFile.getAbsolutePath());
			            }
			            else
			            {
			              log.info("Moving jar '{}' to upgrade plugin '{}'",pluginFile.getAbsolutePath(),key);
			            }
					}
					moveJarToUninstalledDirectory(pluginFile);//移运jar文件到已卸载目录下
			          if ("uninstall".equals(props.get("type")))
			          {
			            removeStateFromStore(this.store, key);
			            this.pluginsUpdateproperties.remove(key);
			          }
				}catch(Exception e){
					 throw new PluginException("Error encountered while trying to remove plugin file " + pluginFile.getPath(), e);
				}
			}
		}
	}
	
	private void removeStateFromStore(PluginStateStore store, String key) {
		PluginManagerState currentState = store.loadPluginState();
		currentState.removeState(key);
		store.savePluginState(currentState);
	}
	
	private void moveJarToUninstalledDirectory(File pluginFile)
			throws FileNotFoundException, IOException {
		if (pluginFile == null) {
			return;
		}
		File uninstalledPlugin = new File(getUninstalledPluginsDirectory(),pluginFile.getName());
		if (!uninstalledPlugin.exists()) {
			uninstalledPlugin.createNewFile();
		}
		IOUtil.copy(pluginFile, uninstalledPlugin);
		pluginFile.delete();
	}

	public void uninstall(Plugin plugin) {

	}

	private Map<String, Map<Object,Object>> getUpdatesMap() {
		Map<String,Map<Object, Object>> updatesMap = new HashMap<String, Map<Object, Object>>();
		for(Object obj : this.pluginsUpdateproperties.getKeys()){
			String key = (String) obj;
			updatesMap.put(key, getPluginUpdate(key));
		}
		return updatesMap;
	}
	
	private Map<String,File> getInstalledPluginJars() throws PluginParseException {
		Map<String,File> installedPlugins =  new HashMap<String, File>();
		File installedPluginsDirectory = getInstalledPluginsDirectory();
		File[] pendingPluginFiles = installedPluginsDirectory.listFiles(jarFileFilter);
		for(File pendingPluginFile : pendingPluginFiles){
			installedPlugins.put(getKeyFrom(pendingPluginFile), pendingPluginFile);
		}
		return installedPlugins;
	}
	private String getKeyFrom(File pluginFile) throws PluginParseException {
		return getKeyFrom(new FilePluginJar(pluginFile));
	}
	private String getKeyFrom(FilePluginJar filePluginJar) throws PluginParseException {
		DescriptorParser descriptorParser = this.descriptorParseFactory.getInstance(filePluginJar.getFile("atlassian-plugin.xml"));
		String key = descriptorParser.getKey();
		if(key ==null){
			throw new  PluginParseException("Plugin key could not be found in atlassian-plugin.xml");
		}
		return key;
	}
	private Map<Object, Object> getPluginUpdate(String key) {
		Properties props = new Properties();
		ByteArrayInputStream bais;
		try{
			bais = new ByteArrayInputStream(this.pluginsUpdateproperties.getText(key).getBytes("ISO-8859-1"));
		}catch(UnsupportedEncodingException e){
			throw new Error("不支持ISO-8859-1编码!");
		}
		try{
			props.load(bais);
		}catch(IOException e){
			log.error(e.getMessage(),e);
			return null;
		}finally{
			try{
				bais.close();
			}catch(IOException e){
				log.error(e.getMessage(),e);
			}
		}
		return Collections.unmodifiableMap(props);
	}
	public void start() {
		if(isConfigured()){
			log.info("正在启动插件系统，插件路径:{}",getPluginsDirectory().getAbsolutePath());
			this.started = createPluginDirectories();
		}
	}
	
	public boolean isStarted(){
		return this.started;
	}

	private boolean createPluginDirectories() {
		return createDirectoryIfNecessary(getPluginsDirectory())
				&& createDirectoryIfNecessary(getInstalledPluginsDirectory())
				&&  createDirectoryIfNecessary(getUninstalledPluginsDirectory())
				&&  createDirectoryIfNecessary(getPendingPluginsDirectory());
	}

	private File getUninstalledPluginsDirectory() {
		if(!isConfigured()){
			return null;
		}
		return new File (getPluginsDirectory(),"uninstalled");
	}

	private File getPendingPluginsDirectory() {
		if(!isConfigured()){
			return null;
		}
		return new File (getPluginsDirectory(),"pending");
	}

	private File getInstalledPluginsDirectory() {
		if(!isConfigured()){
			return null;
		}
		return new File (getPluginsDirectory(),"installed");
	}

	private boolean createDirectoryIfNecessary(File dir) {
		if(!dir.exists() && !dir.mkdirs()){
			log.error("不能创建插件目录'{}',动态插件将不能启用！",dir.getAbsolutePath());
			return false;
		}
		return true;
	}

	private File getPluginsDirectory() {
		if(!isConfigured()){
			return null;
		}
		return new File (this.appProperties.getString("cms.plugins"));
	}
	public PluginLoader getInstalledPluginsLoader() {
		if (!this.started)
	    {
	      return NULL_LOADER;
	    }
	    return new InstalledPluginsLoader(getInstalledPluginsDirectory());
	}

	private static final PluginLoader NULL_LOADER = new PluginLoader() {

	    public boolean supportsAddition()
	    {
	      return false;
	    }

	    public boolean supportsRemoval()
	    {
	      return false;
	    }

	    public void removePlugin(Plugin plugin) throws PluginException
	    {
	    }

		public Collection<Plugin> loadAllPlugins(ModuleDescriptorFactory moduleDescriptorFactory) {
			return Collections.emptyList();
		}

	  };
	  
	  private static class InstalledPluginsLoader implements PluginLoader
	  {
	    private final PluginLoader loader;
	    public InstalledPluginsLoader(File path)
	    {
	      this.loader = new ClassLoadingPluginLoader(path,new DefaultPluginFactory());
	    }

	    public boolean supportsAddition()
	    {
	      return false;
	    }

	    public boolean supportsRemoval()
	    {
	      return false;
	    }

//	    public Collection<Plugin> removeMissingPlugins()
//	    {
//	      throw new UnsupportedOperationException("This PluginLoader does not support removal.");
//	    }


	    public void removePlugin(Plugin plugin) throws PluginException
	    {
	      throw new PluginException("This PluginLoader does not support removal.");
	    }


		public Collection<Plugin> loadAllPlugins(ModuleDescriptorFactory moduleDescriptorFactory) throws PluginParseException {
			return this.loader.loadAllPlugins(moduleDescriptorFactory);
		}
	  }

}
